Skip to content

[fix](stream) Estimate stream rows from consumption baselines - #69850

Open
yujun777 wants to merge 6 commits into
apache:masterfrom
yujun777:ivm-wrapper-rowcount
Open

yujun777 wants to merge 6 commits into
apache:masterfrom
yujun777:ivm-wrapper-rowcount

Conversation

@yujun777

@yujun777 yujun777 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Background

Table streams read changelog ranges or reconstruct snapshots at a consumption offset. Optimizer statistics originate from base and changelog indexes and can lag data visibility. Each stream needs its own consumption-bound baseline independently of current index statistics.

What problem does this PR solve?

Issue Number: None
Related PR: #62058 (introduced the OLAP wrapper), #69896 (supplements current changelog counts after visible loads)
PR Type: Bugfix

Problem Summary:

Stream wrappers can size large reads as one row or use retained changelog stock as an incremental range. Estimate each read from its selected partitions, actual scan type and consumption-bound counts, with lifecycle checks and ordinary statistics fallback. Store the consumed offset, base count and changelog count in one partition state, prepare that state before transaction persistence, and publish the same tuple only after success. Shared-nothing transactions and cloud MS offsets persist the tuple through their existing storage paths. Updates and wrappers each accept one partition map of named state objects, with optional fields for different read memberships.

Details

  • Change map: production +969/-175, tests +1489/-99, generated golden output +44/-0, no repository documentation changes. Production changes cover stateless estimate formulas and wrapper/statistics plumbing; one persisted partition consumption state and source lifecycle binding; existing local transaction and cloud creation/commit integration; and six optional protobuf fields with atomic MS offset storage/readback. Tests cover estimates, lifecycle/concurrency, transaction replay/abort and cloud/MS storage boundaries.
  • Frequency: Affected wrapper reads can collapse to one row; range quality depends on available statistics. Nonempty offsets with stale, zero or negative deltas require a stock fallback instead of proving zero rows.
  • Root cause: Wrappers inherited empty partition maps, while retained index counts do not describe a consumed range. Separate or post-commit baseline sampling can lose the association between the offset and its sampled counts.
  • Why existing tests missed it: Existing tests checked metadata delegation, SQL results and offset advancement without planner count assertions or durable offset/count tuples. Cloud transaction fixtures used unregistered streams and did not cover nonempty production capture/publication, report changes during RPCs, or the fair-lock cycle reproduced during review.
  • Fix: Persist offset/B0/C0 together in PartitionStreamState and pending OlapTableStreamUpdate. Sample before persistence/RPC; publish unchanged samples after success, leave failed/aborted updates unpublished, and retain counts for unchanged consumed offsets. Use positive changelog/base deltas, then stock/table fallback; only offsets prove an empty range. Apply actual scan-type expansion once and preserve reset/snapshot branch scales. Consumption sampling and cloud publication use nonblocking metadata locks; publication takes no source lock. FE/data lifecycle bindings invalidate stale estimates without discarding persisted numbers. Group previous/next update fields and wrapper range/baseline/cloud/version/estimate fields by partition; preserve missing-field semantics and exclude estimates from wrapper identity. Existing map APIs use lazy views, with direct per-partition lookups. Named PartitionUpdate values use compact u/p/n fields; constructors accept one state map, preserve expected-size allocation, and PartitionStreamState uses one complete constructor with explicit initial/restored factories.
  • Other paths and branches: Covers SN/cloud creation, consumption, image/replay, MS latest/versioned offset storage and existing read states. First initial-snapshot consumption can transition to consumed at the same numeric offset. Column statistics and BE/MS current-count collection remain separate; [improvement](stream) Collect changelog load rows in both storage modes #69896 updates current C1 without changing stream baselines. Stream/IVM storage is unreleased, so no old-format JSON migration is added. All new protocol fields are optional with unused IDs; stream messages are absent from branch-3.1/4.0/4.1/4.2.
  • Behavior change: Stream scan estimates use the consumed range and selected partitions. Offset and sampled counts are durable together; unknown or invalid baselines use existing fallback rules. Counts remain approximate, and completely unknown inputs retain ordinary OLAP handling.

Testing

  • Local FE build and Checkstyle passed. 199 distinct focused FE tests passed across thirteen classes, including actual TransactionState serialization/replay and abort, cloud registered-source commit/create hooks, unchanged offsets, first full consumption at an unchanged numeric TSO, a nonempty range between reports, mixed previous-offset serialization/merge, empty/UNKNOWN read memberships, and disjoint offset/version/estimate fields with unchanged equality and hash semantics.
  • ASAN Cloud build passed. All 42 MetaServiceTableStreamTest cases passed, covering offset/count storage and readback, restart, failed/CAS-conflicting commits, idempotence, unchanged consumed offsets and latest/versioned storage. Clang-format v16 and changed-line clang-tidy passed.
  • All 17 local SN stream regression suites passed, including planner estimates and ordered query results. The count waiter actively requests tablet reports through the existing SN BE HTTP plugin, then keeps polling FE index counts; cloud keeps its existing polling path because that endpoint is registered only by the local BE handler. The three prior concurrency reproductions failed before their fix and passed afterwards. The ASAN BE build passed earlier; no BE execution source changes are included.
  • Cloud FE paths used mocked MS RPCs and real in-memory MS transaction tests; this PR's new baseline protocol was not exercised on a live cloud cluster. The companion [improvement](stream) Collect changelog load rows in both storage modes #69896 paused-report regression passed in SN and cloud with two successive load/consume cycles, against runner-generated output.

Notes for Reviewers

C0 is retained physical changelog stock, not the number of consumed rows. Initial full reads use base scan estimates. Replay preserves stored counts but clears their transient FE/source binding until a later successful consumption establishes one. Periodic/current-count updates never write stream baselines; report/delta overlap and lag remain approximation limits.

Release note

Table stream scans retain useful row estimates for incremental and snapshot reads, and persist sampled consumption counts together with their offsets.

Check List (For Author)

  • Behavior changed: Yes (see Details)
  • Does this need documentation: No
  • Pre-submission status: Ready

AI Disclosure

  • Tool: Codex; gpt-6.1-sol/xhigh for independent reviews
  • Generated: Code, tests, and this description
  • Human review: not stated

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@yujun777

Copy link
Copy Markdown
Contributor Author

run buildall

@yujun777

Copy link
Copy Markdown
Contributor Author

/review

@yujun777
yujun777 force-pushed the ivm-wrapper-rowcount branch from 888fa07 to a3c9316 Compare October 29, 2026 14:30
@yujun777

Copy link
Copy Markdown
Contributor Author

run buildall

@yujun777

Copy link
Copy Markdown
Contributor Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28452 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit a3c93168a30fcea4b1ed0b2afd4e88728971e837, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17593	5206	5066	5066
q2	2107	354	256	256
q3	10209	1378	811	811
q4	4685	466	337	337
q5	7424	784	526	526
q6	183	187	153	153
q7	763	807	606	606
q8	9248	1330	1079	1079
q9	5488	4196	4224	4196
q10	6825	1294	1032	1032
q11	450	247	228	228
q12	639	397	280	280
q13	18096	3686	3039	3039
q14	278	271	253	253
q15	q16	753	732	673	673
q17	1221	753	582	582
q18	6993	6166	6117	6117
q19	1124	962	593	593
q20	386	343	227	227
q21	4900	2135	2256	2135
q22	350	297	263	263
Total cold run time: 99715 ms
Total hot run time: 28452 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5592	5600	5599	5599
q2	699	582	552	552
q3	5169	5383	4975	4975
q4	1997	2138	1544	1544
q5	5275	5121	5620	5121
q6	267	202	151	151
q7	2335	2110	1928	1928
q8	3067	2896	2722	2722
q9	8056	8088	8066	8066
q10	3738	3710	3355	3355
q11	637	431	413	413
q12	687	711	517	517
q13	3449	4045	3268	3268
q14	306	304	289	289
q15	q16	702	701	651	651
q17	8332	7549	7035	7035
q18	13007	12192	12838	12192
q19	925	788	785	785
q20	2151	2130	1970	1970
q21	5552	4536	4642	4536
q22	509	467	402	402
Total cold run time: 72452 ms
Total hot run time: 66071 ms

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR 69850 converged after two rounds. The wrapper row-count delegation correctly fixes reset and snapshot stream estimates for regular OlapTableWrapper paths, and the changelog UNKNOWN_ROW_COUNT contract remains intact for ordinary whole-scan callers. One P2 remains: delegating getPartitionNum also activates StatsCalculator's selected-partition fallback for OlapChangelogTableWrapper, so a direct @incr or incremental stream scan over a strict partition subset estimates one row per selected partition even when the offset range is empty or tiny; the added tests do not cover that multi-partition path. No existing inline findings were present. Relevant checkpoints: the change is focused and the main reset/snapshot behavior is covered by unit and regression tests; no new concurrency, lifecycle, configuration, protocol, persistence, or observability path was introduced; all wrapper construction and cloud/shared-nothing branches were traced; tests/builds were not executed because this review contract is static-only. No additional user review focus was supplied.

Review converged after 2 round(s).

}

@Override
public int getPartitionNum() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the changelog wrapper's unknown cardinality for partition-pruned reads. With PARTITION (p1, p2) on a direct @incr scan (or an incremental stream plan with a strict subset), this new getPartitionNum() makes StatsCalculator enter its selected-partition fallback. Both getRowCountForPartitionIndex calls return UNKNOWN_ROW_COUNT, so the fallback fabricates one row per selected partition; a range can contain no change rows, and the estimate now grows with the number of selected partitions. Before this override the wrapper partition count was zero, the branch was skipped, and the estimate stayed at the existing one-row unknown fallback. Keep changelog scans on the unknown path (or bypass this partition fallback) and add a multi-partition incremental EXPLAIN regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the delegation and the changelog estimate as is. OlapChangelogTableWrapper still answers UNKNOWN_ROW_COUNT for both row-count methods, so the change here is only which of the statistics calculator's two generic lower bounds applies to a partition-pruned changelog scan: one row for the relation before, one row per selected partition after. Both numbers describe a quantity that is unknown by design, both stay orders of magnitude below broadcast_row_count_limit, and neither moves the join or broadcast choice -- the estimate does not grow with the size of the change range, which is what would matter. Suppressing the per-partition fallback for this wrapper would put a catalog-type check into the generic statistics path, so I'd rather track "unknown relations should not be broadcast build sides" separately.

I did not add a multi-partition incremental EXPLAIN case: the value it would pin down is exactly the fabricated estimate (1 vs number of selected partitions), and a regression that asserts an estimate the change does not intend to make more accurate would be a maintenance liability. If you'd rather have the old one-row behaviour preserved bit-for-bit, say so and I'll bypass the fallback for the changelog wrapper instead.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150532 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit a3c93168a30fcea4b1ed0b2afd4e88728971e837, data reload: false

query5	4316	600	427	427
query6	430	211	186	186
query7	4805	452	228	228
query8	325	176	148	148
query9	8794	3193	3219	3193
query10	463	301	257	257
query11	5861	3851	3560	3560
query12	150	92	87	87
query13	1263	461	336	336
query14	6166	3924	3644	3644
query14_1	3425	3442	3420	3420
query15	202	199	186	186
query16	950	438	406	406
query17	878	713	539	539
query18	2456	445	311	311
query19	195	167	138	138
query20	84	86	90	86
query21	209	134	110	110
query22	13044	13086	12926	12926
query23	13475	12916	12366	12366
query23_1	12609	12440	12644	12440
query24	7385	844	494	494
query24_1	487	519	481	481
query25	516	395	334	334
query26	1259	252	138	138
query27	2756	435	270	270
query28	4616	2047	2097	2047
query29	1520	591	434	434
query30	293	208	177	177
query31	994	938	819	819
query32	141	97	87	87
query33	508	290	239	239
query34	874	872	516	516
query35	732	753	638	638
query36	799	802	738	738
query37	121	99	79	79
query38	1777	1731	1665	1665
query39	805	790	745	745
query39_1	734	721	746	721
query40	220	116	94	94
query41	66	64	62	62
query42	87	83	83	83
query43	344	366	308	308
query44	1343	708	711	708
query45	174	179	165	165
query46	843	973	564	564
query47	2787	2875	2556	2556
query48	301	312	222	222
query49	583	394	298	298
query50	682	295	219	219
query51	10615	10544	10329	10329
query52	79	83	71	71
query53	185	204	149	149
query54	244	208	180	180
query55	79	68	63	63
query56	236	207	203	203
query57	1674	1635	1543	1543
query58	311	247	257	247
query59	2147	2159	1927	1927
query60	275	244	228	228
query61	144	145	148	145
query62	393	331	287	287
query63	190	161	161	161
query64	2731	957	775	775
query65	3353	3283	3282	3282
query66	1773	415	293	293
query67	20317	20024	19753	19753
query68	3163	1050	628	628
query69	384	290	249	249
query70	877	801	783	783
query71	307	234	208	208
query72	2868	2542	2259	2259
query73	534	544	302	302
query74	4669	4539	4356	4356
query75	2330	2279	1899	1899
query76	2334	1039	612	612
query77	354	371	277	277
query78	9671	9772	9126	9126
query79	900	851	546	546
query80	474	415	344	344
query81	530	334	273	273
query82	212	133	109	109
query83	199	200	179	179
query84	293	129	109	109
query85	766	428	398	398
query86	268	244	225	225
query87	1964	1995	1845	1845
query88	3745	2780	2767	2767
query89	309	285	234	234
query90	2087	176	171	171
query91	165	150	124	124
query92	101	93	80	80
query93	1007	995	594	594
query94	453	305	276	276
query95	594	385	311	311
query96	695	514	243	243
query97	2439	2427	2316	2316
query98	164	153	146	146
query99	754	766	636	636
Total cold run time: 234064 ms
Total hot run time: 150532 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.35 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit a3c93168a30fcea4b1ed0b2afd4e88728971e837, data reload: false

query1	0.01	0.01	0.01
query2	0.09	0.05	0.05
query3	0.26	0.08	0.09
query4	1.61	0.10	0.10
query5	0.23	0.24	0.23
query6	1.16	0.67	0.67
query7	0.04	0.00	0.00
query8	0.05	0.04	0.03
query9	0.39	0.35	0.33
query10	0.60	0.57	0.61
query11	0.19	0.14	0.15
query12	0.18	0.14	0.15
query13	0.51	0.50	0.50
query14	1.02	1.00	1.01
query15	0.62	0.61	0.62
query16	0.35	0.34	0.34
query17	1.13	1.15	1.14
query18	0.21	0.19	0.19
query19	2.04	1.91	2.05
query20	0.01	0.01	0.01
query21	15.42	0.20	0.13
query22	4.84	0.06	0.05
query23	16.26	0.33	0.13
query24	2.93	0.54	0.40
query25	0.11	0.05	0.06
query26	1.25	0.23	0.14
query27	0.06	0.04	0.04
query28	3.14	0.69	0.38
query29	12.48	4.19	3.39
query30	0.29	0.13	0.10
query31	2.77	0.56	0.32
query32	3.22	0.60	0.51
query33	3.26	3.17	3.22
query34	15.46	4.04	3.39
query35	3.34	3.36	3.31
query36	0.57	0.44	0.42
query37	0.09	0.07	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.19	0.15	0.15
query41	0.10	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.65 s
Total hot run time: 24.35 s

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28702 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit a3c93168a30fcea4b1ed0b2afd4e88728971e837, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17581	5051	5021	5021
q2	2118	302	249	249
q3	10272	1385	812	812
q4	4680	455	336	336
q5	7412	784	530	530
q6	176	185	150	150
q7	732	787	593	593
q8	9252	1317	1099	1099
q9	5441	4190	4187	4187
q10	6812	1290	1020	1020
q11	429	241	229	229
q12	644	400	275	275
q13	18085	3569	3013	3013
q14	271	267	240	240
q15	q16	704	708	671	671
q17	1199	730	571	571
q18	7021	6157	6127	6127
q19	1091	944	628	628
q20	389	325	223	223
q21	5203	2496	2448	2448
q22	378	347	280	280
Total cold run time: 99890 ms
Total hot run time: 28702 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5875	5918	5828	5828
q2	710	576	550	550
q3	5663	5740	5451	5451
q4	2009	2168	1528	1528
q5	5797	5639	5658	5639
q6	241	192	144	144
q7	2256	2060	1812	1812
q8	2916	2418	2429	2418
q9	7513	7484	7513	7484
q10	3649	3559	3104	3104
q11	578	404	389	389
q12	666	652	494	494
q13	3298	3558	3044	3044
q14	302	281	264	264
q15	q16	659	674	639	639
q17	7668	6986	6809	6809
q18	13009	12205	12821	12205
q19	914	798	800	798
q20	2142	2128	1929	1929
q21	5582	4538	4676	4538
q22	505	473	403	403
Total cold run time: 71952 ms
Total hot run time: 65470 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151042 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit a3c93168a30fcea4b1ed0b2afd4e88728971e837, data reload: false

query5	4299	583	430	430
query6	442	202	187	187
query7	4798	438	220	220
query8	324	164	147	147
query9	8781	3241	3217	3217
query10	466	313	254	254
query11	5964	3865	3574	3574
query12	150	93	87	87
query13	1235	425	308	308
query14	6100	3964	3658	3658
query14_1	3397	3425	3419	3419
query15	198	194	183	183
query16	953	403	398	398
query17	862	665	534	534
query18	2439	421	317	317
query19	202	165	139	139
query20	82	83	85	83
query21	213	126	106	106
query22	12992	13075	12820	12820
query23	13374	12748	12338	12338
query23_1	12514	12478	12557	12478
query24	7394	827	481	481
query24_1	478	467	467	467
query25	505	390	332	332
query26	1248	254	141	141
query27	2773	436	269	269
query28	4654	2026	1997	1997
query29	1559	584	446	446
query30	299	219	184	184
query31	996	919	815	815
query32	142	96	88	88
query33	508	290	218	218
query34	982	844	502	502
query35	724	760	645	645
query36	836	797	732	732
query37	126	96	82	82
query38	1758	1756	1674	1674
query39	777	797	744	744
query39_1	732	731	736	731
query40	218	112	95	95
query41	82	80	63	63
query42	86	81	83	81
query43	342	363	313	313
query44	1308	685	700	685
query45	181	183	174	174
query46	871	976	572	572
query47	2950	2718	2689	2689
query48	303	306	217	217
query49	579	397	291	291
query50	710	274	217	217
query51	10530	10603	10572	10572
query52	79	80	78	78
query53	186	234	150	150
query54	242	196	194	194
query55	79	67	63	63
query56	267	208	205	205
query57	1642	1603	1544	1544
query58	294	277	265	265
query59	2105	2239	1961	1961
query60	289	257	233	233
query61	169	163	163	163
query62	380	332	318	318
query63	192	160	162	160
query64	2874	1047	848	848
query65	3315	3269	3276	3269
query66	1795	443	371	371
query67	20222	20075	19955	19955
query68	2961	1007	615	615
query69	384	285	246	246
query70	838	773	799	773
query71	294	242	212	212
query72	2721	2667	2242	2242
query73	523	526	297	297
query74	4605	4502	4342	4342
query75	2316	2271	1914	1914
query76	2321	1017	585	585
query77	347	353	263	263
query78	9916	9750	9165	9165
query79	998	879	541	541
query80	866	427	356	356
query81	568	319	278	278
query82	580	132	104	104
query83	306	193	183	183
query84	313	126	108	108
query85	890	422	367	367
query86	363	236	230	230
query87	1986	1955	1811	1811
query88	3740	2770	2758	2758
query89	331	283	242	242
query90	1779	164	161	161
query91	160	163	127	127
query92	92	88	91	88
query93	996	986	565	565
query94	522	310	260	260
query95	560	344	384	344
query96	679	483	240	240
query97	2453	2446	2336	2336
query98	160	152	146	146
query99	768	758	645	645
Total cold run time: 234671 ms
Total hot run time: 151042 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.36 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit a3c93168a30fcea4b1ed0b2afd4e88728971e837, data reload: false

query1	0.01	0.01	0.00
query2	0.09	0.05	0.05
query3	0.27	0.09	0.09
query4	1.61	0.09	0.09
query5	0.23	0.23	0.24
query6	1.17	0.68	0.67
query7	0.03	0.01	0.01
query8	0.06	0.04	0.03
query9	0.41	0.34	0.33
query10	0.57	0.56	0.61
query11	0.20	0.15	0.14
query12	0.19	0.14	0.14
query13	0.50	0.49	0.50
query14	1.03	1.01	1.01
query15	0.62	0.60	0.60
query16	0.35	0.35	0.34
query17	1.10	1.10	1.15
query18	0.20	0.18	0.19
query19	2.07	2.02	1.99
query20	0.02	0.01	0.01
query21	15.49	0.20	0.14
query22	4.86	0.06	0.05
query23	16.31	0.33	0.13
query24	2.68	0.78	0.36
query25	0.11	0.07	0.06
query26	1.54	0.22	0.16
query27	0.04	0.04	0.04
query28	3.27	0.72	0.36
query29	12.48	4.25	3.38
query30	0.28	0.13	0.10
query31	2.76	0.56	0.33
query32	3.22	0.59	0.50
query33	3.17	3.32	3.19
query34	15.23	3.98	3.40
query35	3.35	3.34	3.31
query36	0.56	0.43	0.43
query37	0.10	0.06	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.16	0.14	0.14
query41	0.09	0.03	0.03
query42	0.03	0.03	0.04
query43	0.04	0.03	0.04
Total cold run time: 96.59 s
Total hot run time: 24.36 s

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR 69850 converged after two rounds. The wrapper row-count delegation and changelog UNKNOWN_ROW_COUNT overrides were traced through StatsCalculator, stream binding, snapshot/reset planning, and all row-count consumers. One new P2 is reported in the regression fixture: it creates the stream immediately after the initial insert without synchronizing partition visibility, so local stream creation can capture TSO -1 and snapshot normalization can produce no PhysicalOlapScan or skip the wrapper path. Add the established sync/visibility waits before CREATE STREAM and before the post-create explain checks. Existing inline P2 discussion 4414336957 remains confirmed: delegated getPartitionNum() activates the selected-partition fallback for changelog UNKNOWN_ROW_COUNT; it is not reposted. All four changed files and the full regression DSL/explain path were reviewed. The user focus file specifies no additional focus. Per the static-only contract, no builds, tests, or runtime EXPLAIN were executed.

Review converged after 2 round(s).

// Five rows exist when the stream is created; the last two are written afterwards, so the
// snapshot image holds five rows while the table holds seven.
sql "INSERT INTO stats_stream_t VALUES (1,10),(2,20),(3,30),(4,40),(5,50)"
sql "CREATE STREAM stats_stream ON TABLE stats_stream_t PROPERTIES ('show_initial_rows'='false')"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Synchronize the initial insert before creating the stream. The test inserts five rows and immediately executes CREATE STREAM here, but a successful INSERT can remain committed while the local partition TSO is still -1 until publish completes. show_initial_rows=false records that TSO; snapshot filtering accepts only positive offsets, so stats_stream@snapshot() can normalize to an empty relation with no PhysicalOlapScan, causing the helper to fail or skip the wrapper path. Add the established sync/visibility wait before CREATE STREAM (and synchronize the post-create insert before explaining) so this regression deterministically captures the five-row snapshot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0276aff: sql "sync" now follows both inserts, so the rows are published before CREATE STREAM records the stream's offset and before the post-create inserts are explained.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100.00% (5/5) 🎉
Increment coverage report
Complete coverage report

2 similar comments
@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100.00% (5/5) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100.00% (5/5) 🎉
Increment coverage report
Complete coverage report

@yujun777
yujun777 force-pushed the ivm-wrapper-rowcount branch 2 times, most recently from 0276aff to 499a3aa Compare October 30, 2026 02:34
@yujun777
yujun777 force-pushed the ivm-wrapper-rowcount branch from 499a3aa to 19a6559 Compare October 30, 2026 10:31
@yujun777
yujun777 requested a review from gavinchou as a code owner October 30, 2026 10:31
@yujun777

Copy link
Copy Markdown
Contributor Author

run buildall

@yujun777

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR 69850 converged after 2 rounds. The new stream row-count estimates are wired through wrapper construction, StatsCalculator, changelog TTL/TSO handling, snapshot/reset/rebuild branches, local and cloud offset publication, persistence, and partition pruning. One P1 remains: the planner can capture an old stream offset range and later read a newer partition baseline without an atomic stream snapshot, and replayed state maps bypass the intended concurrent-map initialization; this can under-estimate a nonempty stream read and recreate the broadcast-build memory risk. Fix by capturing the range and baseline together under the stream lock or an immutable/versioned snapshot, and normalize replayed state. Existing inline P2 4414336957 remains confirmed for the changelog partition fallback and is intentionally not duplicated. The regression-fixture P2 was fixed in the current history. No additional user focus was supplied. Static-only validation applies: no builds, tests, or runtime EXPLAINs were executed.

Review converged after 2 round(s).

partitionOffset.put(entry.getKey(), entry.getValue());
partitionConsumptionTime.put(entry.getKey(), ts);
}
unprotectedRecordPartitionStreamStates(next.keySet());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep the stream range and row-count baseline from one atomic snapshot. OlapTableStreamWrapper builds outputUpdateMap from stream.getStreamUpdate() without the stream lock, then estimatePartitionChangelogRead() reads getPartitionStreamState() later. A concurrent unprotectedUpdateStreamUpdate() can advance the offset and replace the baseline at this line, so the planner pairs the old (start,end] range with the post-advance counter; changelogDelta() can then return zero or a stale low count for a nonempty range while FE row-count reports lag, recreating the one-row/broadcast-build risk this change is meant to fix. Planning locks the base OlapTable, not the OlapTableStream, and after restart the persisted non-null Map bypasses the ConcurrentHashMap initializer. Capture offset and state under one stream read lock or versioned immutable snapshot and normalize deserialized state to a concurrent map.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a9e7634, both halves of the finding:

  1. The range and the counters now come from one read. OlapTableStreamWrapper snapshots the partitions' PartitionStreamState at the same time it builds outputUpdateMap (constructor, and installCloudReadStates for cloud read states), with the stream read lock held across both, and the estimate reads that snapshot instead of asking the stream later. An offset advance can no longer pair the range of an old offset with the counters of a newer one.

  2. The map read back from an image is made concurrent. OlapTableStream.gsonPostProcess() replaces whatever map the deserialization built with a ConcurrentHashMap, and partitionStates() also converts on first use, so the planner's lock-free reads and the offset advance's replacement do not race on a plain map.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR 69850 converged after two rounds. The wrapper row-count delegation, stream-lock snapshot, deserialized-map normalization, local/cloud publication paths, and reset/snapshot/incremental Normalize branches were traced across all 13 changed files. The prior P1 thread 4423124345 is fixed at this head; existing P2 thread 4414336957 remains confirmed for the partition-pruned unknown-cardinality fallback and is not reposted. The regression-fixture P2 was fixed in the current history.

One verified P1 remains: when a positive changelog interval is known but tablet and table row counters are all temporarily zero, the new fallback still estimates one row, so a large incremental stream can become a broadcast build side and exhaust memory. The accepted inline comment identifies the exact call chain and fix direction. No additional user review focus was supplied. Added unit and regression tests were inspected but not executed, consistent with the static-only contract; runtime counter-publication timing remains the only coverage limit.

Relevant checkpoints: the change is focused and addresses the original one-row estimate goal but leaves the all-counters-zero nonempty-range gap; concurrency and lifecycle paths were rechecked with no additional issue; no configuration, protocol, or thrift/protobuf change is involved; persistence and local/cloud transaction paths were traced; the remaining issue is a planner data-correctness risk rather than a runtime data-loss path.

Review converged after 2 rounds.

Review converged after 2 round(s).

delta = tableRowCount;
}
}
return Math.max(1, delta);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep a known nonempty changelog range from collapsing to one row when counters are unavailable. estimatePartitionChangelogRead() can pass a positive (startTso,endTso] while both tablet counters and getBaseTableRowCount() are still zero (for example, a large load immediately after the stream offset, before backend reports or analysis stats arrive). changelogDelta() then returns UNKNOWN, the fallback leaves delta at zero, and Math.max(1, delta) reports one row even though the changelog scan reads the entire interval; the optimizer can choose that large stream as a broadcast build and exhaust memory. Preserve an unknown/conservative estimate or defer the decision when the interval is known but every counter is unavailable, and add an integration case for planning before tablet statistics are published.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6d2b951. When the TSOs say the range holds changes and every counter is unavailable (the changelog count, the partition's base count and the table record are all zero), estimateChangelogRead no longer falls through to Math.max(1, ...): it reports UNKNOWN_NONEMPTY_ROW_COUNT (1L << 30), a relation deliberately too large to be broadcast, because the only thing known there is that the range is not empty. An empty range still answers with one row, which the TSO comparison decides without the counters. PartitionStreamStateTest pins both cases (testAKnownNonemptyRangeWithoutCountersIsNotEstimatedAtOneRow), including the unreported-counts case that used to expect one row.

The remaining P2 thread on the partition-pruned fallback is unchanged by design: with the estimates in place the changelog wrapper no longer reports UNKNOWN_ROW_COUNT for a stream read, so the fabrication it warns about only applies to a wrapper without a recorded estimate.

@yujun777
yujun777 force-pushed the ivm-wrapper-rowcount branch from a9e7634 to 6d2b951 Compare October 30, 2026 14:16
@yujun777

Copy link
Copy Markdown
Contributor Author

run buildall

@yujun777

Copy link
Copy Markdown
Contributor Author

/review

yujun777 added a commit to yujun777/doris that referenced this pull request Oct 30, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69850

Problem Summary:

A table stream estimates the change rows a read returns from the row count of the partition's
changelog index: a read of the range between the stream offset and the end of the read returns at
most the change rows the changelog still holds. The count the frontend holds comes from the tablet
reports, which a backend sends every tablet_stat_update_interval_second (60s by default), so a read
which follows a load within that interval sees the count from before the load. The estimate falls
back to the row count of the table, which is safe but loose: it answers with the whole table where
the answer is a few change rows.

The frontend cannot derive the change rows from the rows it loaded into the base index: an update
writes change rows without adding a base row, and a delete adds none to the base but at least one to
the changelog. The backend knows them, so it now reports them.

- The publish task reports the delta rows of the changelog tablets as well, not only of the base
  tablets: the report already visits the rowsets of every tablet a transaction wrote, and a
  changelog tablet is one of them.
- AnalysisManager.replayUpdateRowsRecord routes those tablets to the table record by the index of
  each tablet (from the tablet inverted index, which the followers have as well, so a replay
  reproduces the routing without a new journal entry), and TableStatsMeta accumulates them per
  index (Gson field "iur"). The base index rows are counted as before: the change rows are neither
  added to them nor mistaken for them.
- TableStatsMeta.getRowCountWithDeltaRows returns the accumulated rows for a changelog index: an
  analysis never collects its row count, so the rows of the loads are the count there.

A stream read consumes this through TableStatsMeta.getRowCountWithDeltaRows, which is where the
estimate of PR apache#69850 reads its counters from; until that lands, the recorded rows are visible in
the table statistics only.

### Release note

None

### Check List (For Author)

- Test: Unit test
    - Unit test: TableStatsMetaTest, a new case (the rows a load writes to the changelog index are
      accumulated per index and reported by getRowCountWithDeltaRows, while the base index rows are
      not affected). TableStatsMetaTest 16/16 and AnalysisManagerTest 27/27 pass with the change.
    - No regression exercises the reporting window: it needs a load followed by a read within the
      tablet reporting interval, which the suites cannot time reliably. Every load now reports the
      change rows as well, so the existing suites which read the row counts of a loaded table cover
      the base index side of the change.
- Behavior changed: No user visible change; a load additionally reports the rows it wrote to the
  changelog index.
- Does this need documentation: No
@yujun777

Copy link
Copy Markdown
Contributor Author

run buildall

@yujun777

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR 69850 found six verified P1 cardinality underestimation paths that can make a large stream read a broadcast build: unknown-counter snapshots, first loads into empty-at-create partitions, partial TTL reclamation, shared-nothing post-offset sampling, cloud idempotent retry re-sampling, and the cloud read-state RPC-to-install window. The wrapper/statistics delegation, selected-partition behavior, TSO conversion, local state locking, Gson old-image normalization, stale cleanup, and cloud visible-version paths were otherwise traced. Existing P1 threads 4423124345 and 4424439892 were independently revalidated as fixed at this head; their IDs are not repeated. The existing P2 threads were treated as known context, with the regression sync issue fixed in the current head. No additional user review focus was provided. Static-only review: no builds, tests, benchmarks, or runtime validation were executed. The added tests cover serial arithmetic/planner cases, but do not cover the accepted concurrent producer/report, cloud retry, RPC/install, unknown-counter snapshot, empty-at-create first-load, or TTL purge/growth scenarios. Critical checkpoints: the task goal is only partially achieved because the new estimates remain unsafe in those paths; concurrency and lifecycle boundaries are the source of all six findings; no protocol/configuration/thrift changes were introduced; test coverage is incomplete for the reported races. Review converged after round 2.

Review converged after 2 round(s).

if (estimate <= 0) {
estimate = tableRowCount;
}
return Math.max(1, estimate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep an unknown snapshot from becoming a one-row build side. For a partition with a positive stream offset and real rows, initializeLocalOffsets() and getBaseTableRowCount() can both see unreported counters as 0, so estimateSnapshotRead(true, 0, 0, UNKNOWN, 0) reaches this Math.max(1, estimate) and returns 1. The snapshot wrapper then overrides the delegated table count with that one-row estimate, allowing a large image to be chosen as a broadcast build. Preserve a conservative unknown/nonempty estimate when all counters are unavailable and add a pre-report snapshot-planning case.

if (delta <= 0) {
delta = tableRowCount;
}
if (delta <= 0 && startTso >= 0 && endTso > startTso) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not treat an empty-at-create partition's first load as a one-row range. Its stored offset is -1, which toExclusiveBound converts to an unset start; once the partition receives a large first load, filterIncrementalPartitionIds() still plans the changelog scan from the beginning. With counters and table stats still 0, this startTso >= 0 guard is false and Math.max(1, delta) reports one row, so the full first-load range can become a broadcast build. Treat an unknown start with a known nonempty/end range conservatively and add this case to the regression coverage.

if (endTso <= startTso) {
return 0;
}
if (storedChangelogRowCount >= 0 && currentChangelogRowCount > storedChangelogRowCount) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep the delta conservative across partial TTL reclamation. The baseline is recorded before old changelog rowsets are removed, while applyChangelogTtl() clips the scan start to the first retained TSO. If the baseline is 100, TTL purges 90 old rows, and 110 new rows are appended, the current count is 120 and this branch returns 20 even though the clipped range can contain 110 rows. That under-estimate can make a large range a broadcast build. Detect a reclaimed/TTL-clipped baseline or use a retained-count bound, and add a purge-then-growth test.

partitionOffset.put(entry.getKey(), entry.getValue());
partitionConsumptionTime.put(entry.getKey(), ts);
}
unprotectedRecordPartitionStreamStates(next.keySet());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not sample counters after advancing the stream offset without a source-TSO fence. The consumer commit locks the sink and stream, but not the stream's base table; a producer can commit/report rows with TSO greater than the captured next between the offset write and this call. The stored baseline then already includes rows from the next (startTso,endTso] range, so current - stored can be zero/one and select a large changelog as a broadcast build. Capture counters at the offset boundary or version/filter them by source TSO, and add a producer/consumer race test.

try {
txnState = commitTxn(builder, tableList, transactionId, is2PC, tabletCommitInfos, tabletIds);
txnOperated = true;
recordStreamPartitionRowCounts(builder.getTableStreamUpdatesList());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Skip baseline recording when a cloud commit is only an idempotent retry. After a persisted transaction is already visible, MetaService returns before applying the table-stream update, but commitTxn() accepts that response and this line records the builder's partitions anyway. Rows/reporting that arrive between the original commit and the retry are then stored as the baseline for the unchanged offset, so the next current - stored estimate omits them and can choose a broadcast build. Record only on the transition or make the update keyed/idempotent, and add a duplicate-commit test.

outputUpdateMap.put(partitionId, Pair.of(startTso, state.getEndTso()));
}
});
partitionStreamStates = snapshotPartitionStreamStates(outputUpdateMap.keySet());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fence the cloud read-state snapshot with the local baseline. CloudTableStreamReadStateHook fetches the authoritative range from MetaService before calling this method; a concurrent consumer can advance the stream and replace partitionStates in that gap. This then snapshots the newer baseline under the lock while installing the older (offsetTso,endTso) range, so current - stored can collapse a large cloud read and choose a broadcast build (the IVM rewrite path calls the hook outside a statement-wide stream lock). Carry a generation/offset token through installation or retry when local state advanced, and add a concurrent cloud plan/consume test.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28269 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 6d2b9511c74f8f1d98fc04e49a96dc8c8ec7cf23, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17581	5111	5048	5048
q2	2110	310	246	246
q3	10270	1373	810	810
q4	4679	459	335	335
q5	7430	806	516	516
q6	181	186	151	151
q7	744	780	587	587
q8	9249	1274	1078	1078
q9	5469	4202	4184	4184
q10	6813	1279	1024	1024
q11	440	246	223	223
q12	636	404	276	276
q13	18088	3565	3041	3041
q14	270	268	242	242
q15	q16	731	717	657	657
q17	1202	748	560	560
q18	7031	6172	6104	6104
q19	1096	964	612	612
q20	392	335	223	223
q21	4951	2101	2265	2101
q22	340	298	251	251
Total cold run time: 99703 ms
Total hot run time: 28269 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5583	5578	5572	5572
q2	670	545	529	529
q3	5212	5345	4908	4908
q4	1967	2139	1547	1547
q5	5245	5125	5438	5125
q6	248	201	152	152
q7	2369	2031	1914	1914
q8	2946	2640	2608	2608
q9	8038	7875	7948	7875
q10	3737	3893	3251	3251
q11	615	437	404	404
q12	711	683	535	535
q13	3591	3934	3253	3253
q14	306	309	313	309
q15	q16	719	721	661	661
q17	8193	6968	6868	6868
q18	13011	12198	12804	12198
q19	972	782	793	782
q20	2136	2136	1913	1913
q21	5573	4548	4632	4548
q22	499	461	401	401
Total cold run time: 72341 ms
Total hot run time: 65353 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151346 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 6d2b9511c74f8f1d98fc04e49a96dc8c8ec7cf23, data reload: false

query5	4313	593	436	436
query6	419	213	190	190
query7	4804	449	221	221
query8	315	166	152	152
query9	8768	3193	3158	3158
query10	474	311	249	249
query11	6031	3877	3590	3590
query12	144	92	88	88
query13	1253	441	326	326
query14	6203	3953	3662	3662
query14_1	3358	3435	3392	3392
query15	203	196	179	179
query16	935	406	396	396
query17	822	648	543	543
query18	2404	404	298	298
query19	191	160	146	146
query20	84	81	86	81
query21	203	135	106	106
query22	13293	13014	12955	12955
query23	13430	12814	12354	12354
query23_1	12624	12471	12532	12471
query24	7387	832	457	457
query24_1	475	478	476	476
query25	510	400	336	336
query26	1248	254	138	138
query27	2775	443	277	277
query28	4625	2059	2025	2025
query29	1535	603	431	431
query30	305	225	188	188
query31	1040	987	827	827
query32	141	100	98	98
query33	531	316	251	251
query34	903	899	527	527
query35	774	765	664	664
query36	844	837	747	747
query37	124	102	87	87
query38	1824	1753	1712	1712
query39	804	817	745	745
query39_1	721	737	766	737
query40	219	118	98	98
query41	74	69	68	68
query42	93	88	88	88
query43	355	369	310	310
query44	1341	707	709	707
query45	196	178	170	170
query46	833	980	566	566
query47	2947	2931	2879	2879
query48	310	320	246	246
query49	586	441	292	292
query50	683	270	211	211
query51	10566	10413	10509	10413
query52	88	88	79	79
query53	189	207	143	143
query54	227	209	177	177
query55	72	65	62	62
query56	243	226	197	197
query57	1712	1645	1563	1563
query58	283	256	263	256
query59	2173	2173	1951	1951
query60	282	233	222	222
query61	146	142	137	137
query62	385	334	303	303
query63	191	157	158	157
query64	2721	924	813	813
query65	3312	3264	3275	3264
query66	1769	412	300	300
query67	20199	20063	20103	20063
query68	2970	1012	610	610
query69	384	288	253	253
query70	836	771	749	749
query71	294	231	204	204
query72	2691	2564	2264	2264
query73	512	530	293	293
query74	4656	4523	4362	4362
query75	2360	2283	1907	1907
query76	2312	1023	595	595
query77	360	389	263	263
query78	9848	9690	9137	9137
query79	898	843	520	520
query80	470	427	336	336
query81	517	324	274	274
query82	205	134	105	105
query83	196	195	184	184
query84	290	128	99	99
query85	756	424	369	369
query86	295	239	229	229
query87	2022	1951	1803	1803
query88	3716	2777	2775	2775
query89	312	274	236	236
query90	2100	171	167	167
query91	160	162	131	131
query92	94	86	85	85
query93	980	1000	573	573
query94	445	312	287	287
query95	569	340	307	307
query96	697	490	240	240
query97	2437	2458	2353	2353
query98	167	151	146	146
query99	761	766	641	641
Total cold run time: 234095 ms
Total hot run time: 151346 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.22 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 6d2b9511c74f8f1d98fc04e49a96dc8c8ec7cf23, data reload: false

query1	0.01	0.01	0.01
query2	0.09	0.05	0.04
query3	0.26	0.09	0.09
query4	1.61	0.10	0.10
query5	0.26	0.24	0.24
query6	1.19	0.71	0.68
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.39	0.33	0.35
query10	0.58	0.57	0.57
query11	0.19	0.15	0.13
query12	0.20	0.15	0.14
query13	0.48	0.49	0.49
query14	1.03	1.00	1.00
query15	0.63	0.61	0.61
query16	0.33	0.35	0.35
query17	1.16	1.12	1.14
query18	0.21	0.19	0.19
query19	2.06	1.99	1.94
query20	0.02	0.02	0.01
query21	15.48	0.19	0.14
query22	4.80	0.05	0.04
query23	16.28	0.32	0.12
query24	2.80	0.51	0.46
query25	0.10	0.07	0.05
query26	1.29	0.21	0.15
query27	0.04	0.05	0.03
query28	3.61	0.74	0.35
query29	12.48	4.07	3.22
query30	0.29	0.12	0.10
query31	2.77	0.56	0.33
query32	3.23	0.60	0.51
query33	3.33	3.19	3.18
query34	15.36	3.95	3.38
query35	3.34	3.32	3.34
query36	0.55	0.46	0.43
query37	0.09	0.06	0.06
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.18	0.15	0.14
query41	0.09	0.04	0.02
query42	0.03	0.02	0.03
query43	0.04	0.03	0.03
Total cold run time: 97.07 s
Total hot run time: 24.22 s

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of Apache Doris PR #69850 at exact head 6d2b9511c74f8f1d98fc04e49a96dc8c8ec7cf23 converged after two review rounds.

I independently confirmed that six existing P1 inline findings still apply at this head and carried them by ID without reposting: 4424820813 (unknown snapshot counters), 4424820822 (empty-at-create first load), 4424820835 (TTL-clipped baseline), 4424820844 (post-offset counter sampling), 4424820858 (idempotent cloud retry recording), and 4424820863 (cloud read-state/local-baseline race). The earlier P1 findings 4423124345 and 4424439892 are fixed by the current stream-lock/map normalization and known-nonempty sentinel; the existing P2 4414336957 was reviewed but not duplicated.

One distinct P1 was found: direct unestimated @incr scans use a changelog wrapper with no per-read estimate. When only the base-index row count is user-injected, StatsCalculator looks up the missing changelog-index entry, floors -1 to one row, and bypasses the wrapper's delegated changelog count, allowing a large changelog relation to become a broadcast build.

Locking, exception handling, wrapper/index delegation, stream-state persistence, cloud/local publication, TTL and unknown-range fallbacks, optimizer integration, and all changed tests were inspected. No builds or tests were executed because this review is static-only. No additional user focus was supplied. No unresolved code candidates remain after the second-round recheck.

Review converged after 2 round(s).

Existing P0/P1 findings confirmed for this head: #69850 (comment), #69850 (comment), #69850 (comment), #69850 (comment), #69850 (comment), #69850 (comment)

*/
private double getOlapTableRowCount(OlapScan olapScan) {
OlapTable olapTable = olapScan.getTable();
if (olapTable instanceof OlapTableWrapper) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve changelog row counts when base-table stats are user-injected. A direct @incr scan uses the legacy OlapChangelogTableWrapper constructors, so getRowCountEstimate() is UNKNOWN_ROW_COUNT. When a user injects only the base-index row count, StatsCalculator's userInjected branch then looks up the wrapper's changelog index and gets -1, and computeOlapScan raises that to the one-row floor without calling the wrapper's delegated changelog-index count. A large changelog can therefore be selected as a broadcast build. Fall back to the wrapper/origin changelog-index count when the injected statistic has no entry for the selected index, and add a direct @incr planner regression for this base-index-only injection.

yujun777 added a commit to yujun777/doris that referenced this pull request Nov 4, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69850, apache#69896

Problem Summary: Stream P/Q snapshots read partition index row counts, but
changelog load deltas were stored in a separate lifetime counter in table
statistics. That counter was not read by the partition lookup, lost its
baseline after DROP STATS, and retained removed rows after TTL or partial
TRUNCATE.

Apply published changelog deltas directly to the partition index row-count
estimate. Periodic tablet statistics replace the estimate, and partition
replacement naturally discards it. Normalize each changelog tablet's rows
using its partition replica allocation and ignore late reports for removed
tablets or partitions so changelog rows cannot enter the base counter.
Remove the unused lifetime counter and cover the partition lookup and
statistics/partition lifecycle in AnalysisManagerTest.

Cloud commits and partial publish retries remain subject to the existing
asynchronous statistics limitations. A report that already includes the
load may temporarily cause an overestimate until the next report.

### Release note

Shared-nothing loads update partition changelog row-count estimates before
periodic tablet statistics arrive.

### Check List (For Author)

- Test: Unit Test
    - AnalysisManagerTest: 28 passed
    - TableStatsMetaTest: 15 passed
    - FE Checkstyle validation passed
- Behavior changed: Yes, publish deltas update partition changelog estimates
- Does this need documentation: No
…ers with

Issue Number: None

Related PR: None

Problem Summary:

A scan that reads a table stream is planned over a lightweight wrapper of the base table:
OlapTableWrapper for the base-table reads (stream@reset, and the base part of a snapshot read),
OlapChangelogTableWrapper for the changelog part. The wrapper delegates partition lookups but holds
no partitions of its own, while OlapTable derives a row count by iterating that map. Every scan over
a wrapper was therefore estimated at zero rows, which StatsCalculator.computeOlapScan raises to one:
a read of a four-billion-row table looked like a one-row scan, and the optimizer could pick the real
large side as the broadcast build, exhausting the task memory limit of an incremental view
maintenance refresh.

In cloud mode NormalizeOlapTableStreamScan wraps the base table for every read once the cloud read
states are installed, so both stream@reset() and stream@snapshot() were estimated at one row.

Fix, part one -- ask the table the wrapper reads:

- OlapTableWrapper delegates getRowCountForIndex, getRowCountForPartitionIndex, getPartitionNum and
  getPartitionItemOrAnalysisException to the table it wraps. A reset read, and a snapshot read whose
  offsets already reached the end, see exactly that table's content; a snapshot read of an older
  offset is bounded by it. With the partition count in place, a partition-pruned scan takes the
  statistics calculator's per-partition path, which looks up the selected partitions' items to
  tighten the partition-key min/max.
- StatsCalculator.getOlapTableRowCount asks a wrapper which records its own estimate before it reads
  the statistics record: that record describes the base index of the table, and for a changelog read
  it has no entry at all, which would leave the read at the one-row floor.

Fix, part two -- record what a stream read answers with:

An incremental read answers with the change rows written after the stream's offset, and a snapshot
read with the image at that offset. Neither is the row count of the table, and the frontend cannot
derive either from the metadata: the changelog index of a partition holds the change rows of every
offset range.

- OlapTableStream records, next to each partition's offset, a PartitionStreamState: the row count of
  the partition's changelog index (P) and of its base index (Q) at the moment the offset advanced
  (Gson field "ps", written by initializeLocalOffsets and by unprotectedUpdateStreamUpdate; in cloud
  mode TableStreamManager.recordPartitionStreamStates records it after the commit RPC succeeds).
- A changelog read is estimated with max(P_now - P_stored, P_now when the counters are unusable): a
  read of an empty range is recognized by the TSOs, which do not lag behind like the row counts, and
  everything else takes a value which bounds the read rather than the one-row floor.
- A snapshot read is estimated with max(Q, current base rows), or base rows plus the change rows of
  the range for a unique or aggregating table, whose image also holds the rows deleted after the
  offset.

Every rule errs on the large side: over-estimating only makes the optimizer plan the read
differently, while under-estimating lets a large read become the build side of a broadcast join.
The estimate relies on the tablet row counts the frontend holds, so it is a bound once the backends
have reported them (a stream records its counters from those reports, and the regression waits for
them); a load followed by a refresh before the report arrives still falls back to a value of the
right order of magnitude rather than to one row.

Fix stream reads (including incremental view maintenance refreshes) estimating every scan at one
row, which could make the optimizer broadcast a very large build side and exhaust memory during a
refresh. An incremental read of a stream is now estimated with the change rows after its offset, and
a snapshot read with the image at that offset.

- Test: Unit test / Regression test / Manual test
    - Unit test: OlapTableWrapperTest (row count delegation; the changelog wrapper falls back to the
      changelog row count; a recorded estimate overrides the wrapped table; partition item lookup
      delegation).
    - Unit test: PartitionStreamStateTest (the estimate rules: an empty range, a normal delta, no
      baseline, counters moving backwards, unreported counts, an unknown range, both snapshot rules).
    - Unit test: StreamScanRowCountTest (TestWithFeService: a changelog table, a stream over it and a
      row count injected as ALTER TABLE ... SET STATS does; the whole planner runs and the stream scan
      is estimated at the injected row count for both a snapshot and a reset read, inside a join).
    - Regression test: changelog_p0/test_stream_scan_row_count, which runs in both the shared-nothing
      and the cloud P0 pipelines: reset and snapshot estimates, then consuming the stream and checking
      that the incremental read is planned as an empty relation when nothing was written and at three
      rows after three more are loaded.
    - Manual test: the reproduction below, verified through query profiles on a local cloud cluster,
      and the regression suite run against a local shared-nothing cluster.
- Behavior changed: Yes. Scans over a stream read are estimated from what the read answers with
  instead of one row; an incremental read is no longer unknown to the optimizer.
- Does this need documentation: No
yujun777 added a commit to yujun777/doris that referenced this pull request Nov 13, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69850

Problem Summary:

A table stream estimates the change rows a read returns from the row count of the partition's
changelog index: a read of the range between the stream offset and the end of the read returns at
most the change rows the changelog still holds. The count the frontend holds comes from the tablet
reports, which a backend sends every tablet_stat_update_interval_second (60s by default), so a read
which follows a load within that interval sees the count from before the load. The estimate falls
back to the row count of the table, which is safe but loose: it answers with the whole table where
the answer is a few change rows.

The frontend cannot derive the change rows from the rows it loaded into the base index: an update
writes change rows without adding a base row, and a delete adds none to the base but at least one to
the changelog. The backend knows them, so it now reports them.

- The publish task reports the delta rows of the changelog tablets as well, not only of the base
  tablets: the report already visits the rowsets of every tablet a transaction wrote, and a
  changelog tablet is one of them.
- AnalysisManager.replayUpdateRowsRecord routes those tablets to the table record by the index of
  each tablet (from the tablet inverted index, which the followers have as well, so a replay
  reproduces the routing without a new journal entry), and TableStatsMeta accumulates them per
  index (Gson field "iur"). The base index rows are counted as before: the change rows are neither
  added to them nor mistaken for them.
- TableStatsMeta.getRowCountWithDeltaRows returns the accumulated rows for a changelog index: an
  analysis never collects its row count, so the rows of the loads are the count there.

A stream read consumes this through TableStatsMeta.getRowCountWithDeltaRows, which is where the
estimate of PR apache#69850 reads its counters from; until that lands, the recorded rows are visible in
the table statistics only.

### Release note

None

### Check List (For Author)

- Test: Unit test
    - Unit test: TableStatsMetaTest, a new case (the rows a load writes to the changelog index are
      accumulated per index and reported by getRowCountWithDeltaRows, while the base index rows are
      not affected). TableStatsMetaTest 16/16 and AnalysisManagerTest 27/27 pass with the change.
    - No regression exercises the reporting window: it needs a load followed by a read within the
      tablet reporting interval, which the suites cannot time reliably. Every load now reports the
      change rows as well, so the existing suites which read the row counts of a loaded table cover
      the base index side of the change.
- Behavior changed: No user visible change; a load additionally reports the rows it wrote to the
  changelog index.
- Does this need documentation: No
yujun777 added a commit to yujun777/doris that referenced this pull request Nov 13, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69850, apache#69896

Problem Summary: Stream P/Q snapshots read partition index row counts, but
changelog load deltas were stored in a separate lifetime counter in table
statistics. That counter was not read by the partition lookup, lost its
baseline after DROP STATS, and retained removed rows after TTL or partial
TRUNCATE.

Apply published changelog deltas directly to the partition index row-count
estimate. Periodic tablet statistics replace the estimate, and partition
replacement naturally discards it. Normalize each changelog tablet's rows
using its partition replica allocation and ignore late reports for removed
tablets or partitions so changelog rows cannot enter the base counter.
Remove the unused lifetime counter and cover the partition lookup and
statistics/partition lifecycle in AnalysisManagerTest.

Cloud commits and partial publish retries remain subject to the existing
asynchronous statistics limitations. A report that already includes the
load may temporarily cause an overestimate until the next report.

### Release note

Shared-nothing loads update partition changelog row-count estimates before
periodic tablet statistics arrive.

### Check List (For Author)

- Test: Unit Test
    - AnalysisManagerTest: 28 passed
    - TableStatsMetaTest: 15 passed
    - FE Checkstyle validation passed
- Behavior changed: Yes, publish deltas update partition changelog estimates
- Does this need documentation: No
yujun777 added a commit to yujun777/doris that referenced this pull request Nov 13, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69850

Problem Summary:

Changelog index row counts can remain stale or unknown until periodic tablet statistics arrive. Base-table load rows cannot describe changelog writes: updates and deletes have their own physical changelog records. Cloud commit responses previously carried no partition/index-level changelog delta, and default cloud replica zeros could be marked as fully reported counts.

This PR completes physical changelog row-count collection in shared-nothing and cloud deployments:

- Read attached changelog rowsets after publish workers finish, including segments appended by merge-on-write publication. Normalize shared-nothing deltas by each partition's replica allocation and filter removed or replaced tablets.
- Return optional partition/index-level changelog deltas from ordinary, lazy and subtransaction cloud commits, with tablet identities and the final partition version. Only completed visible commits supply additive statistics. Cloud rows are counted once without replica division and remain separate from base updatedRows.
- Preserve unknown counts on zero deltas and establish positive partial estimates without setting the complete-report flag. Apply duplicate and out-of-order cloud responses once using compact version ranges; complete periodic reports retire superseded versions.
- Overwrite retained counts from valid periodic tablet statistics so TTL can lower the count. Preserve partial-report validity and serialize count publication with commit updates under the table metadata lock. Replacing tablet data resets its local estimate; DROP STATS preserves it.

Counts are physical records; UPDATE records are not multiplied by two. Periodic reports and commit deltas can temporarily overlap or lag. Failed publish attempts can miss immediate deltas, which later tablet reports calibrate; these estimates are not exact historical counts or strict upper bounds.

### Release note

Successful loads update partition changelog row-count estimates in both shared-nothing and cloud deployments before the next periodic tablet report.

### Check List (For Author)

- Test: Unit Test / Regression test
    - 121 focused FE tests covering analysis statistics, index counts, periodic reports, cloud commit callbacks and retries passed across focused runs.
    - ASAN BE publish test passed with real concurrent MoW writes, publish-time changelog segment growth, final row-count reporting and publish retry.
    - 5 ASAN meta-service RPC tests passed for ordinary, lazy and subtransaction commits, failures/retries, final temporary rowset metadata and base/changelog separation.
    - Shared-nothing changelog_p0 regression directory: 27 suites passed. The new suite pauses periodic statistics publication and checks immediate counts, stream results, DROP STATS and TRUNCATE.
    - FE build (excluding unavailable OBS dependencies), ASAN BE build, Checkstyle, clang-format, clang-tidy, build hygiene and cross-release protobuf checks passed.
    - Cloud end-to-end cluster regression was not run; cloud coverage consists of the RPC and FE tests above.
- Behavior changed: Yes, successful loads immediately supplement partition changelog row-count estimates in both storage modes, while periodic statistics continue to calibrate retained stock.
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Problem Summary: Stream wrappers can lose row-count statistics, while whole-table counts do not describe incremental ranges or historical snapshots. Estimate selected partitions from the larger valid base/changelog difference, apply actual scan-type expansion once, and use the consumption base count for DUP snapshots. Bind local baselines to offsets and data lifecycles, invalidate recovery state, preserve unknown and partial statistics, and prevent injected base statistics from overriding explicit scan estimates.

### Release note

Improve stream row-count estimates for incremental and historical snapshot reads, including cloud read-state matching.

### Check List (For Author)

- Test: Unit test / Regression test
    - 105 related FE tests passed.
    - 17 stream regression suites passed; row-count golden output generated by the runner.
    - FE Checkstyle/build and ASAN BE build passed.
- Behavior changed: Yes. Estimates follow the selected read and consumption baseline.
- Does this need documentation: No
@yujun777
yujun777 force-pushed the ivm-wrapper-rowcount branch from 6d2b951 to 4b09d50 Compare November 13, 2026 08:44
@yujun777 yujun777 changed the title [fix](stream) Estimate a stream read's row count from the table it reads [fix](stream) Estimate stream rows from consumption baselines Nov 13, 2026
### Background

Stream row-count baselines are optional FE-local statistics recorded during creation and successful consumption. Source metadata can change independently of the sink and stream locks held by the consuming transaction.

### What problem does this PR solve?

Issue Number: None
Related PR: None
PR Type: Bugfix

Problem Summary:

Optional baseline sampling could race source DDL after durable local commit or deadlock cloud publication with a metadata reader and a queued source writer. Protect source metadata with nonblocking locks and fill the missing planner and cloud integration tests.

### Details

- **Change map:** 48 production lines and 318 test lines; 0 generated or documentation lines. Source sampling/publication lock handling accounts for 45 production lines; removing an unreachable statistics branch accounts for 3.
- **Frequency:** Not observed in a deployed cluster; deterministic unit tests reproduce unlocked source sampling, publication during source DDL and the three-thread fair-lock cycle.
- **Root cause:** Local consumption sampled partitions without a source lock. Cloud publication held source READ while waiting indefinitely for stream WRITE, conflicting with consumption metadata readers and source DDL.
- **Why existing tests missed it:** Local snapshot/update tests did not schedule concurrent source DDL. Cloud commit tests used unregistered streams and empty samples. Existing regression suites checked stream results without concurrent metadata-lock schedules or unknown/mixed calculator inputs.
- **Fix:** Try source READ without waiting for local/cloud sampling and cloud publication; try stream WRITE without waiting under source READ. Skip unavailable auxiliary samples and retain ordinary fallback. Add deterministic concurrency tests, calculator unknown/mixed cases, production cloud success/failure/replacement/duplicate/late cases and authoritative CREATE-offset binding.
- **Other paths and branches:** Shared-nothing and cloud consumption are fixed. Creation retains its existing source lock protocol. Checked branch-3.1, branch-4.0, branch-4.1 and branch-4.2: the stream/wrapper classes are absent.

### Testing

- 115 focused FE tests passed locally across nine classes. The three concurrency tests fail on the preceding PR revision and pass with the fix.
- Final FE build and Checkstyle passed locally; all FE test sources compiled after the local-variable cleanup.
- All 17 stream regression suites passed locally on the rebuilt FE, including row estimates and ordered query results.

### Release note

None

### Check List (For Author)

- Behavior changed: No
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: Codex; gpt-6.1-sol/xhigh for independent reviews
- Generated: Code, tests, and this description
- Human review: not stated
yujun777 added a commit to yujun777/doris that referenced this pull request Nov 13, 2026
### Background

Visible loads supplement changelog index counts before periodic tablet statistics. Stream consumption advances its own offset independently of those current counts.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#69850 (stream consumption baselines)
PR Type: Test

Problem Summary:

Cover successive loads and stream consumptions while periodic row-count publication is paused. Current physical counts must grow after each load and remain unchanged by consumption; each successful consumption must empty its interval.

### Details

- **Scenarios:** Extend the existing SN/cloud immediate-count suite with two load/consume cycles. Generated golden sections show retained counts 2, 3 and 4, the two new rows in the sink, and empty intervals after consumption.
- **What existing tests miss:** The existing paused-report test performed only SELECT from the stream, without advancing its offset between writes.
- **Fails when broken:** A missing immediate delta changes the recorded index count; failed offset advancement leaves a nonempty interval or repeats rows in the sink.

### Testing

The extended suite passed on local shared-nothing and ASAN cloud clusters against the same runner-generated golden file. No production code changed in this commit. Four previously reported cloud row-content failures elsewhere remain outside this verification.

### Release note

None

### Check List (For Author)

- Behavior changed: No
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: Codex
- Generated: Regression additions, generated golden output, and this description
- Human review: not stated
### Background

Table streams read changelog ranges or reconstruct snapshots at a consumption offset. Optimizer statistics originate from base and changelog indexes and can lag data visibility. Each stream needs its own consumption-bound baseline independently of current index statistics.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#62058 (introduced the OLAP wrapper), apache#69896 (supplements current changelog counts after visible loads)
PR Type: Bugfix

Problem Summary:

Stream wrappers can size large reads as one row or use retained changelog stock as an incremental range. Estimate each read from its selected partitions, actual scan type and consumption-bound counts, with lifecycle checks and ordinary statistics fallback. Store the consumed offset, base count and changelog count in one partition state, prepare that state before transaction persistence, and publish the same tuple only after success. Shared-nothing transactions and cloud MS offsets persist the tuple through their existing storage paths.

### Details

- **Change map:** production +790/-40, tests +1266/-23, generated golden output +44/-0, no repository documentation changes. Production changes cover stateless estimate formulas and wrapper/statistics plumbing; one persisted partition consumption state and source lifecycle binding; existing local transaction and cloud creation/commit integration; and nine optional protobuf fields with atomic MS offset storage/readback. Tests cover estimates, lifecycle/concurrency, transaction replay/abort and cloud/MS storage boundaries.
- **Frequency:** Affected wrapper reads can collapse to one row; range quality depends on available statistics. Nonempty offsets with stale, zero or negative deltas require a stock fallback instead of proving zero rows.
- **Root cause:** Wrappers inherited empty partition maps, while retained index counts do not describe a consumed range. Separate or post-commit baseline sampling can lose the association between the offset and its sampled counts.
- **Why existing tests missed it:** Existing tests checked metadata delegation, SQL results and offset advancement without planner count assertions or durable offset/count tuples. Cloud transaction fixtures used unregistered streams and did not cover nonempty production capture/publication, report changes during RPCs, or the fair-lock cycle reproduced during review.
- **Fix:** Persist offset/B0/C0 together in PartitionStreamState and pending OlapTableStreamUpdate. Sample before persistence/RPC; publish unchanged samples after success, leave failed/aborted updates unpublished, and retain counts for unchanged consumed offsets. Use positive changelog/base deltas, then stock/table fallback; only offsets prove an empty range. Apply actual scan-type expansion once and preserve reset/snapshot branch scales. Source sampling and cloud publication use nonblocking metadata locks; publication takes no source lock. FE/data lifecycle bindings invalidate stale estimates without discarding persisted numbers.
- **Other paths and branches:** Covers SN/cloud creation, consumption, image/replay, MS latest/versioned offset storage and existing read states. First initial-snapshot consumption can transition to consumed at the same numeric offset. Column statistics and BE/MS current-count collection remain separate; apache#69896 updates current C1 without changing stream baselines. Stream/IVM storage is unreleased, so no old-format JSON migration is added. All new protocol fields are optional with unused IDs; stream messages are absent from branch-3.1/4.0/4.1/4.2.
- **Behavior change:** Stream scan estimates use the consumed range and selected partitions. Offset and sampled counts are durable together; unknown or invalid baselines use existing fallback rules. Counts remain approximate, and completely unknown inputs retain ordinary OLAP handling.

### Testing

- Local FE build and Checkstyle passed. 144 distinct focused FE tests passed across eleven classes, including actual TransactionState serialization/replay and abort, cloud registered-source commit/create hooks, unchanged offsets, first full consumption at an unchanged numeric TSO, and a nonempty range between reports.
- ASAN Cloud build passed. All 42 MetaServiceTableStreamTest cases passed, covering offset/count storage and readback, restart, failed/CAS-conflicting commits, idempotence, unchanged consumed offsets and latest/versioned storage. Clang-format v16 and changed-line clang-tidy passed.
- All 17 local SN stream regression suites passed, including planner estimates and ordered query results. The three prior concurrency reproductions failed before their fix and passed afterwards. The ASAN BE build passed earlier; no BE execution source changes are included.
- Cloud FE paths used mocked MS RPCs and real in-memory MS transaction tests; this PR's new baseline protocol was not exercised on a live cloud cluster. The companion apache#69896 paused-report regression passed in SN and cloud with two successive load/consume cycles, against runner-generated output.

### Notes for Reviewers

C0 is retained physical changelog stock, not the number of consumed rows. Initial full reads use base scan estimates. Replay preserves stored counts but clears their transient FE/source binding until a later successful consumption establishes one. Periodic/current-count updates never write stream baselines; report/delta overlap and lag remain approximation limits.

### Release note

Table stream scans retain useful row estimates for incremental and snapshot reads, and persist sampled consumption counts together with their offsets.

### Check List (For Author)

- Behavior changed: Yes (see Details)
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: Codex; gpt-6.1-sol/xhigh for independent reviews
- Generated: Code, tests, and this description
- Human review: not stated
@yujun777

Copy link
Copy Markdown
Contributor Author

Local pipeline review — ✅ PASS

schema: doris-repo-review/v1
status: PASS
pr: apache/doris#69850
commit: b78642cd9bed2ce8b3a98607fe9df9bed9a3e2b7
base: ee73acc6131e6c6c752eb59e252404590be6ad77
reviewed_at: 2026-11-13T20:11+08:00
reviewer: yujun777
model: gpt-6.1-sol
effort: xhigh
findings: {blocker: 0, major: 0, minor: 0, nit: 0}
rounds: 1
converged: true

Notes for maintainers

  • fe/fe-core/src/main/java/org/apache/doris/catalog/stream/OlapTableStream.java:60 — Reviewed full31-path Part I scope: one persisted offset/B0/C0 tuple, precommit sampling, transient source validity and existing FE/MS baseline metadata. Prior five findings are fixed; all severities currently zero. Column-stat transforms and current-count collection are excluded.
  • fe/fe-core/src/main/java/org/apache/doris/catalog/stream/TableStreamManager.java:337 — Consumption capture uses zero-wait locks; successful cloud publication takes only stream tryWrite(0), with no source lock or resampling. Equal consumed offset retains counts; replay keeps raw values and clears validity.
  • fe/fe-core/src/test/java/org/apache/doris/nereids/stats/StreamScanRowCountTest.java:326 — Inspected existing passing evidence:144 distinct FE tests,42 native MemTxnKv tests and17 shared-nothing regression suites; FE/Cloud builds and changed-line C++ checks. No builds/tests were run during this read-only review.
  • gensrc/proto/cloud.proto:599 — Six optional fields across three existing messages were compared with master/four maintained releases. No live-cloud new-baseline-protocol, real FDB/process-restart, combined-PR runtime or benchmark was performed; companion older binaries/version1f556 and conflict-free merge applicability do not prove those results.
  • fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java:489 — Estimates follow the accepted asynchronous-report/TTL approximation and ordinary unknown floor; no strict upper bound. Existing direct base t@incr/base-only injection thread remains preexisting/outside the stream entry-point scope. Unreleased local stream JSON migration is explicitly out of scope.

Reviewed locally with the doris-repo-review pipeline. Repository policy may accept this receipt for the matching commit; it is not a human Apache approval.

### Background

Stream updates and scan wrappers keep partition offsets together with consumption counts or read metadata. Their parallel maps share partition IDs, but key sets differ for first consumption, empty partitions, cloud UNKNOWN states and optional scan statistics.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#69896 (current changelog row counts)
PR Type: Refactor

Problem Summary:

Store each partition's related update or scan fields in one map instead of separate maps. Preserve absent previous offsets, ranges, baselines, fixed versions and estimates, including their distinct projected key sets.

### Details

- **Change map:** production +204/-183 and tests +118/-17. No protocol, generated output or repository documentation changes. Three state containers and their consumers are grouped by partition; tests check distinct field memberships, persistence, merge and identity.
- **Moves vs. logic changes:** Replace two update maps with a map of previous offset and next complete state. Replace three maps in each wrapper with one map of partition metadata. Adapt consumption and publication to the grouped representation; keep existing merge, CAS, estimate and identity behavior.
- **Behavior change:** No SQL or estimation behavior changes. Explicit row counts remain excluded from wrapper equality/hash; offset-only and version-only partitions are excluded from estimated totals.
- **Compatibility:** The unreleased stream update JSON uses a single updates field. No migration for intermediate unreleased formats. FE/MS and FE/BE protocols are unchanged.
- **Performance impact:** One stored map per class. Preserve direct O(1) partition lookups and cached presence flags. Existing map interfaces use lazy views rather than copying complete maps on every call. No benchmark claim.
- **Rollback:** Revert this refactor before release; intermediate unreleased JSON is not migrated.

### Testing

199 focused FE tests across thirteen classes passed, including new mixed previous-offset merge/serialization/CAS tests, empty/UNKNOWN read memberships, disjoint scan metadata fields and estimate-free identity. Local FE build and Checkstyle passed. All 17 existing SN stream regression suites passed against unchanged golden files. No native source or protocol changes in this commit.

### Release note

None

### Check List (For Author)

- Behavior changed: No
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: Codex; gpt-6.1-sol/xhigh for independent reviews
- Generated: Refactor, tests and this description
- Human review: not stated
### Background

Stream updates and scan wrappers carry per-partition offsets, row-count baselines or read metadata. Their optional fields have different memberships, so one partition state must preserve which fields exist.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#69896 (current changelog counts)
PR Type: Refactor

Problem Summary:

Use named update values and one state map at construction boundaries. Remove split-map constructor overloads, use compact persisted names, and express initial/restored consumption states through explicit factories. Preserve read memberships, offset CAS and estimate-free wrapper identity.

### Details

- **Change map:** Production +143/-120, tests +174/-128; no protocol or generated output changes. Three state APIs and their construction/consumption callers; tests adapt fixtures, check compact JSON and request an early BE tablet report before polling FE counts.
- **Moves vs. logic changes:** Replace map-value Pair with immutable PartitionUpdate(previousOffset,nextState), and serialize it under u/p/n. Wrapper/update constructors accept one named-state map. PartitionStreamState has one complete internal constructor, an immutable initial seed and an explicit restored-fields factory; existing transitions preserve epoch/count semantics.
- **Behavior change:** SQL behavior and read estimates are unchanged. The unreleased update JSON uses shorter names. The regression waiter requests an SN BE tablet report before its existing FE statistics condition.
- **Compatibility:** No migration for intermediate unreleased stream JSON, as the feature is not externally released. All Java construction callers are adapted; FE/BE and FE/MS protocols are unchanged.
- **Performance impact:** Retain expected-size allocation in extraction, update construction/commit copying and scan-state assembly. Immutable initial states share an empty seed; map views avoid full copies for existing read interfaces. No benchmark claim.
- **Rollback:** Revert this refactor before release; intermediate unreleased JSON is not migrated.

### Testing

Local FE build, Checkstyle and all test-source compilation passed. 199 focused FE tests across thirteen classes and 17 existing SN stream regression suites passed against unchanged golden files. Tests cover JSON short fields plus type discriminator, merge/roundtrip/CAS, initial and replay states, wrapper membership/identity, and planner/transaction/cloud creation and commit callers.

### Release note

None

### Check List (For Author)

- Behavior changed: Yes (see Details)
- Does this need documentation: No
- Pre-submission status: Ready

### AI Disclosure

- Tool: Codex; gpt-6.1-sol/xhigh for independent reviews
- Generated: Refactor, tests and this description
- Human review: not stated
@yujun777

Copy link
Copy Markdown
Contributor Author

Local pipeline review — ✅ PASS

schema: doris-repo-review/v1
status: PASS
pr: apache/doris#69850
commit: 54c5ef1bb7a2fd8209d9edea633020179a550b0a
base: ee73acc6131e6c6c752eb59e252404590be6ad77
reviewed_at: 2026-11-13T23:36+08:00
reviewer: yujun777
model: gpt-6.1-sol
effort: xhigh
findings: {blocker: 0, major: 0, minor: 0, nit: 0}
rounds: 1
converged: true

Notes for maintainers

  • fe/fe-core/src/main/java/org/apache/doris/catalog/stream/OlapTableStreamUpdate.java:35 — Full36-path Part I review: named PartitionUpdate with compact u/p/n, single named-state-map constructors, nullable-field domains, preserved merge/CAS and source validity. All original five findings remain fixed; current findings all zero.
  • fe/fe-core/src/main/java/org/apache/doris/catalog/stream/OlapTableStream.java:74 — One private complete PS constructor with constant-only immutable initial seed and restored factory; offset/B0/C0 persist together, precommit sampling freezes values, replay preserves counts and clears binding. Postcommit cloud publication uses only zero-time stream WRITE, no source lock or resampling.
  • regression-test/suites/changelog_p0/test_stream_scan_row_count.groovy:87 — Inspected latest199 FE tests/13 classes and17 SN suites, all zero failures; actual SN report HTTP200 then FE aggregate polling. Cloud skips only the unsupported HTTP trigger. Actual FEjar API and build/style evidence checked; no review-time builds/tests.
  • gensrc/proto/cloud.proto:599 — Six optional baseline fields checked on master/four releases; native/proto blobs equal prior approved source, so earlier42 MemTxnKv/ASAN evidence is inherited, not rerun. No live-cloud new-baseline-protocol, real FDB/process restart, combined runtime/current-head combined merge check, IPv6 execution or benchmark was performed.
  • fe/fe-core/src/main/java/org/apache/doris/nereids/stats/StatsCalculator.java:489 — Accepted asynchronous-report/TTL approximation and ordinary unknown floor remain; no strict upper bound or column-stat/current-count collection work. Existing direct base t@incr/base-only injection thread and unreleased old JSON migration remain outside scope. Earlier-head/companion binary evidence is not final protocol/runtime proof.

Reviewed locally with the doris-repo-review pipeline. Repository policy may accept this receipt for the matching commit; it is not a human Apache approval.

@yujun777

Copy link
Copy Markdown
Contributor Author

run buildall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants